package elizaengine;

import java.util.ArrayList;
import java.util.Random;

import elizascript.*;

/**
 * The response generator computes the most appropriate decomposition rule
 * and applies one of the corresponding assembly rules.
 * 
 * @author Caroline
 *
 */

public class ResponseGenerator {
	
	// provides access to the eliza script
	ScriptInterop script;
	// InputProcessor provides a processed form of the user input and it's keywords
	InputProcessor inputProcessor;
	ArrayList<KeywordObject> keyWords;
	SubstitutionRules substitutionRules;
	
	public ResponseGenerator(ScriptInterop script, String input) {
		this.script = script;
		inputProcessor = new InputProcessor(script);
		inputProcessor.setUserInput(input);
		substitutionRules = new SubstitutionRules(script);
		keyWords = inputProcessor.getKeywords();
	}
	
	// chooses the most appropriate decomposition rule and forms a reply depending
	// on the type of the rule.
	public String generateResponse(){
		String response= null;
		// most appropriate decomposition rule is calculated
		DecompositionRule rule = chooseMostAppropriateDecompostionRule();
		if(rule == null){
			// if no matching decomposition rule could be found a generic message is randomly picked
			ArrayList<String> possibleResponses = script.getGenericMessage();
			Random random = new Random();
			int chosenResponse = random.nextInt(possibleResponses.size());
			response = possibleResponses.get(chosenResponse);
		}
		
		else if (rule.isResponseRule()) {
			// if the most appropriate decomposition rule is a response rule, a response
			// is formed based on the assembly rule
			response = applyAssemblyRule(rule);
		}
		else if (rule.isCommand()) {
			// since the only command is quit, the program is quit and a leaving message
			// is displayed
			Engine.quit();
			ArrayList<String> possibleResponses = script.getLeavingMessage();
			response = returnRandomMessageFromList(possibleResponses);
		}
		return response;
	}

	
	// the most appropriate decompositionrule is chosen based on how many words of the
	// response it matches and the priority of the keyword
	private DecompositionRule chooseMostAppropriateDecompostionRule() {
		
		KeywordObject currentKeyword;
		int bestMatchingKeywordID = 0;
		DecompositionRule currentRule;
		DecompositionRule bestMatchingRule = null;
		int currentWordMatch;
		int highestWordMatch = 0;
		
		// itereates through every rule of every keyword and calculates the number of words
		// it matches
		for (int i = 0; i < keyWords.size(); i++) {
			currentKeyword = keyWords.get(i);
			for (int j = 0; j < currentKeyword.DecompositionRules.size(); j++) {
				currentRule = currentKeyword.DecompositionRules.get(j);
				currentWordMatch = calculateWordMatch(inputProcessor.getSeparatedUserInput(), currentRule);
				
				// keeps track of the rule with the highest word match
				if (currentWordMatch > highestWordMatch) {
					bestMatchingRule = currentRule;
					highestWordMatch = currentWordMatch;
					bestMatchingKeywordID = currentKeyword.ID;
				}else{
					// if two decompositionrules match the same number of words, the decomposition rule
					// belonging to the keyword with the higher priority is chosen
					if(currentWordMatch == highestWordMatch) {
						if (currentKeyword.ID > bestMatchingKeywordID) {
							bestMatchingRule = currentRule;
							highestWordMatch = currentWordMatch;
							bestMatchingKeywordID = currentKeyword.ID;
						}
					}
				}
			}
		}
		
		return bestMatchingRule;
	}
	
	private String applyAssemblyRule(DecompositionRule decompositionRule) {
		
		String decompRule = decompositionRule.DecompositionRule;
		
		//when using the String.split() method splitting on asterisk, it can't be seen whether it has
		//an asterisk on the end
		boolean endsOnAsterisk =  checkForStarAtPlace(decompRule, decompRule.length()-1);
		
		
		String[] assembParts = decompositionRule.AssemblyRules.get(decompositionRule.nextUsedAssemblyRule).split(" ");
		String[] decompParts = decompRule.split("\\*");
		String[] currentDecompPart;
		String[] input = inputProcessor.getSeparatedUserInput();
		
		//list of the custom input that will replace the asterisks in the decomposition rule
		ArrayList<String> asterisks = new ArrayList<String>();	
		
		int currentStringPosition = 0;
		String currentAsterisk = "";
		boolean belongsToSameAsterisk = true;
		String response = "";

		// iteratates through all sections of the decomposition rule
		for (int i = 0; i < decompParts.length; i++) {
			// splits the current section, skipping it if it is an empty string
			// empty strings occur if a decomposition rule starts on an asterisk, as there are no
			// characters before the first asterisk
			if (!decompParts[i].equals("")) {
				currentDecompPart = decompParts[i].split(" ");
				currentStringPosition += currentDecompPart.length;
			} else {
				i++;
				currentDecompPart = decompParts[i].split(" ");
				currentStringPosition += currentDecompPart.length;
			}
			// adds the custom input between the rule sections to the list 'asterisks'
			for (int j = currentStringPosition; (j < input.length) && (belongsToSameAsterisk); j++) {
				if (!currentDecompPart[0].equals(input[j])) {
					currentAsterisk += input[j] + " ";
				} else {
					belongsToSameAsterisk = false;
					currentStringPosition = j;
				}
			} 
			asterisks.add(currentAsterisk);
			currentAsterisk = "";
		}
		
		// the custom input for the last asterisk is added separately since after the split method is performed,
		// it is not easily recognisable whether there is another asterik at the end
		if(endsOnAsterisk){
			for (int i = currentStringPosition; i < input.length; i++) {
				currentAsterisk += input[i] + " ";
			}
			asterisks.add(currentAsterisk);
		}
		
		// a copy of the asterisks list is formed and post substitution rules are applied
		// it is applied here so that the scripts part of the responses doesn't get affected.
		String[] processedAsterisk = new String[asterisks.size()];
		for (int i = 0; i < asterisks.size(); i++) {
			processedAsterisk[i]= substitutionRules.applyPostsubstitutionRule(asterisks.get(i));
		}
		
		// the gaps in the assembly rules are filled with the corresponding custom asteriks entries
		for (int i = 0; i < assembParts.length; i++) {
			if (assembParts[i].charAt(0) == '%' ){
				int custom = Character.getNumericValue(assembParts[i].charAt(1));
				response += processedAsterisk[custom - 1] + " ";
			}else{
				response += assembParts[i] + " ";
			}
		}
		
		// any excess white space is taken away
		response = response.trim();
		
		// Finally, we increment the next assembly rule index, so we get a different result next time.
		decompositionRule.nextUsedAssemblyRule++;
		if(decompositionRule.nextUsedAssemblyRule >= decompositionRule.AssemblyRules.size()) {
			decompositionRule.nextUsedAssemblyRule = 0;
		}
		
		return response;
	}
	
	// calculates whether a decomposition rule matches the input and if so,
	// how many words match
	private int calculateWordMatch(String[] input, DecompositionRule decompositionRule) {
		
		String decompRule = decompositionRule.DecompositionRule;
		// Convert to lowercase to increase matches.
		String[] decompParts = decompRule.split("\\*");
		String[] currentDecompPart;
		
		boolean matching = false;
		int matchedAt = 0;
		int numberOfMatches=0;

		// keeps track of the users custom input between the rule sections
		// it is used as a form of validation, to ensure that an input has at 
		// least one word in the place of every asterisk of the decompositin rule
		// it gets cleared after each rule segment and counting is restarted
		ArrayList<String> custom = new ArrayList<String>();
		// a rule is only allowed if all the asterisks in the decomposition rule are
		// matched by at least one word
		boolean allowed = true;
		
		// checks if there is an asterisk at the beginning or end that might not be noticed
		// once the String.split() method is applied
		boolean starAtBeginning = checkForStarAtPlace(decompRule, 0);
		boolean starAtEnd =  checkForStarAtPlace(decompRule, decompRule.length()-1);
	
		// iterates through all sections of the decomposition rule while the rule matches
		// the input and there is at least one user input for every asterisk 
		for (int i = 0; (i < decompParts.length) && (allowed); i++) {
			
			//skips empty strings produced by String.split method
			if (!decompParts[i].equals("")) {
				currentDecompPart = decompParts[i].split(" ");
				
				// iterates from the last match through the input
				for (int j = matchedAt; j < input.length; j++) {
					
					// when the input matches the current section of the decomposition rule,
					// the number of matching words are counted
					if (currentDecompPart[0].equals(input[j].toLowerCase())) {
						matching = true;
						for (int k = 0; (k < currentDecompPart.length) && (matching); k++) {
							if ((j + k < input.length) && (input[j + k].toLowerCase().equals(currentDecompPart[k]))) {
								numberOfMatches++;
							} else {
								// matching is set to false if the input and the rule differ
								matching = false;
							}
						}
						// 0 is returned in case the decomposition rule differs from the input
						if (!matching) {
							return 0;
						}
					} else {
						// all words between the rule segments are added 
						custom.add(input[j]);
					}
				}
				// validation of cases when no user input could be collected between rule segments
				if (custom.size() < 1) {
					// if a rule starts with an asterisk, there must be at least one custom input
					// before the first rule segment
					if (i == 0) {
						if (starAtBeginning) {
						allowed = false;
						}
					}
					// if a rule ends with an asterisk, there must be at least one custom input after
					// the last rule segment
					else if (i == (decompParts.length - 1)){
						if (starAtEnd) {
							allowed = false;
						}
					}
					// if there are rule segments on either side of the asterisk, there must be
					// at least one custom input between the rule segments
					else {
						allowed = false;
					}
				}
				// custom is cleared so that it can count user input for the next asterisk
				custom.clear();
			}
		}
		// if the rule is valid, the number of matches is returned
		if (allowed) {
			return numberOfMatches;
		} else {
			return -1;
		}
	}
	
	// chooses a random message from an ArrayList of messages
	private String returnRandomMessageFromList (ArrayList<String> list){
		Random random = new Random();
		String message;
		int chosenMessage = random.nextInt(list.size());
		message = list.get(chosenMessage);
		return message;
	}
	
	// checks if a specified place in a string is an asterisk
	private boolean checkForStarAtPlace (String string, int place){
		boolean hasStar =  false;
		if (string.charAt(place) == '*'){
			hasStar = true;
		}
		return hasStar;
	}
}
